<template>
    <div ref="container" style="height: 100%; width: 100%;" />
    <Teleport to="body">
        <div ref="imgPreviewContainerRef" v-show="state.imgContainerVisible" class="wang-editor-img-preview-container" @click.self="closeImg">
            <div class="tool-bar" @click.stop>
                <div
                    :class="{
                        'operate-icon-container': true,
                        disabled: imgPreview.transform.level - 0.4 <= imgPreview.transform.minLevel
                    }"
                    @click="zoom(imgPreview.transform.level - 0.4)"
                >
                    <img src="./assets/svg/zoomOut.svg" />
                </div>
                <div
                    :class="{
                        'operate-icon-container': true,
                        disabled: imgPreview.transform.level + 0.4 >= imgPreview.transform.maxLevel
                    }"
                    @click="zoom(imgPreview.transform.level + 0.4)"
                >
                    <img src="./assets/svg/zoomIn.svg" />
                </div>
                <div class="operate-icon-container" @click="closeImg">
                    <img src="./assets/svg/close.svg" />
                </div>
            </div>
            <div
                class="img-container"
                :style="{
                    cursor: imgPreview.dragStatus === 2 ? 'grabbing' : ''
                }"
                @click.self="closeImg"
                @touchstart.self="(e) => { e.preventDefault(); closeImg() }"
            >
                <!-- @vue-expect-error -->
                <img ref="imgPreviewRef" draggable="false" :src="imgPreview.src" :style="style" />
            </div>
        </div>
    </Teleport>
</template>

<script lang="ts">
export default { name: 'WangEditor' }
</script>

<script lang="ts" setup>
import { shallowRef, onBeforeUnmount, ref, onMounted, render, h, watch, reactive, nextTick, computed } from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { IEditorConfig } from '@wangeditor/editor'
import editorCss from '@wangeditor/editor/dist/css/style.css?inline' // 引入 css
import { debounce } from 'lodash'

const emits = defineEmits<{
    (e: 'update:mvvmHTML', value: string): void
    (e: 'update:mvvmText', value: string): void
    (e: 'update:mvvmScrollTo', value: string): void
    (e: 'change', value: { headers: any[] }): void
}>()

const props = defineProps({
    mvvmHTML: {
        type: String,
        default: '',
        chineseAlias: '值',
        description: '组件绑定的值'
    },
    mvvmText: {
        type: String,
        default: '',
        chineseAlias: '文本值',
        description: '组件绑定的文本值'
    },
    ReadOnly: {
        type: Boolean,
        default: false,
        chineseAlias: '只读',
        description: '可查看数据或信息，无法修改或编辑'
    },
    mvvmScrollTo: {
        type: String,
        default: '',
        chineseAlias: '标题ID',
        description: '定位到富文本框内的标题位置'
    },
    ImgWidth: {
        type: String,
        default: 'auto',
        chineseAlias: '图片宽度',
        description: '富文本内图片统一宽度'
    }
})

const state = reactive({
    imgContainerVisible: false,
    headers: []
})

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
interface CSS extends CSSStyleDeclaration {
    left: number
    top: number
    width: number
    height: number
}

const imgPreview = reactive({
    src: '',
    style: {
    } as unknown as CSS,
    transform: {
        level: 1,
        maxLevel: 4,
        minLevel: 0.5
    },
    orgRect: {
        left: 0,
        top: 0,
        height: 0,
        width: 0
    },
    dragStatus: 0 // 0未打开图片；1打开图片完毕；2已拖拽
})

const headersMap = computed(() => {
    const map = new Map()
    state.headers.forEach(item => {
        map.set(item.dom, item)
    })
    return map
})

const style = computed(() => {
    return {
        ...imgPreview.style,
        left: imgPreview.style.left + 'px',
        top: imgPreview.style.top + 'px',
        width: imgPreview.style.width * imgPreview.transform.level + 'px',
        height: imgPreview.style.height * imgPreview.transform.level + 'px',
        cursor: imgPreview.dragStatus === 2 ? 'grabbing' : ''
    }
})

const scrollState = {
    scrollBySelf: false,
    scrollByOutSide: false
}

const container = ref()
const editorRef = shallowRef()

const mode  = 'default'
const toolbarConfig = props.ReadOnly ? { toolbarKeys: [] } : {}
const editorConfig: Partial<IEditorConfig> = {
    placeholder: '请输入内容...',
    readOnly: props.ReadOnly,
    MENU_CONF: {
        uploadImage: {
            server: '/api/upload',
            fieldName: 'mulFiles',
            allowedFileTypes: ['image/*'],
            headers: {
                Authorization: 'Bearer ' + localStorage.getItem('token')
            },
            customInsert(res: any, insertFn: (url: string, alt?: string, href?: string) => void) {
                insertFn(res[0].url, res[0].name)
            }
        },
        uploadVideo: {
            server: '/api/upload',
            fieldName: 'mulFiles',
            allowedFileTypes: ['video/*'],
            headers: {
                Authorization: 'Bearer ' + localStorage.getItem('token')
            },
            customInsert(res: any, insertFn: (url: string, alt?: string, href?: string) => void) {
                insertFn(res[0].url, res[0].name)
            }
        }
    }
}

// 组件销毁时，也及时销毁编辑器
onBeforeUnmount(() => {
    editorRef.value?.destroy()
})

const imgPreviewContainerRef = ref<HTMLDivElement>()

let shadowRoot:ShadowRoot = null
const handleCreated = (editor) => {
    editorRef.value = editor // 记录 editor 实例，重要！
    renderFn()

    watch(() => props.ReadOnly, renderFn)
    watch(() => props.mvvmHTML, () => {
        if (props.mvvmHTML === editorValue) return
        // const activeElement = document.activeElement as HTMLInputElement | undefined
        editorRef.value.setHtml(props.mvvmHTML || (props.ReadOnly ? '<p><em> </em></p>' : '<p><br></p>'))
        // activeElement?.focus?.()
        nextTick(() => {
            setHeadersAndObserver()
        })
    })
    setHeadersAndObserver()
    watch(() => props.mvvmScrollTo, () => {
        if (!props.mvvmScrollTo) return
        if (scrollState.scrollBySelf) {
            scrollState.scrollBySelf = false
            return
        }
        const elementInsideShadowDom = shadowRoot.getElementById(props.mvvmScrollTo)
        if (!elementInsideShadowDom) return
        scrollState.scrollByOutSide = true
        elementInsideShadowDom.scrollIntoView({ behavior: 'smooth' }) // , block: 'nearest'
        setTimeout(() => {
            scrollState.scrollByOutSide = false
        }, 1000)
    }, { immediate: true })

    editor.getEditableContainer().addEventListener('click', openImg)

    shadowRoot.addEventListener('focus', (e) => {
        if (!props.ReadOnly) return
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        e.target.blur?.()
    }, true)
}

const openImg = (e: Event) => {
    if (!props.ReadOnly) return
    const target = e.composedPath()?.[0] as unknown as HTMLImageElement
    if (target?.tagName !== 'IMG') return

    imgPreview.orgRect = target.getBoundingClientRect()
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    imgPreview.style.left = imgPreview.orgRect.left,
    imgPreview.style.top = imgPreview.orgRect.top,
    imgPreview.style.width = imgPreview.orgRect.width,
    imgPreview.style.height = imgPreview.orgRect.height
    imgPreview.src = target.src
    state.imgContainerVisible = true
    imgPreview.transform.level = 1
    setTimeout(() => {
        imgPreviewContainerRef.value.style.opacity = '1'

        // 普通缩放
        const { width: documentWidth } = document.body.getBoundingClientRect()
        const containerScale = documentWidth < 650 ? 1 : 0.8
        const widthScale = (documentWidth * containerScale) / imgPreview.orgRect.width
        const heightScale = (window.innerHeight * containerScale) / imgPreview.orgRect.height
        const imgScale = widthScale > heightScale ? heightScale : widthScale
        const imgWidth = imgPreview.orgRect.width * imgScale
        const imgHeight = imgPreview.orgRect.height * imgScale

        imgPreview.style.width = imgWidth
        imgPreview.style.height = imgHeight
        imgPreview.style.left = (documentWidth - imgWidth) / 2
        imgPreview.style.top = (window.innerHeight - imgHeight) / 2

        imgPreview.dragStatus = 1
    })
}

const zoom = (level) => {
    if (imgPreview.dragStatus === 0 || level >= imgPreview.transform.maxLevel || level <= imgPreview.transform.minLevel) return
    const levelDiff = level - imgPreview.transform.level
    imgPreview.transform.level = level

    const widthDiff = imgPreview.style.width * levelDiff / 2
    const heightDiff  = imgPreview.style.height * levelDiff / 2
    imgPreview.style.left -= widthDiff
    imgPreview.style.top -= heightDiff
}

const closeImg = () => {
    imgPreviewContainerRef.value.style.opacity = ''
    setTimeout(() => {
        state.imgContainerVisible = false
        imgPreview.dragStatus = 0
    }, 500)
}

const elementsMap = new Map<Element, { show: boolean }>()
const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
        const value = elementsMap.get(entry.target)
        if (value) value.show = entry.isIntersecting
    })
    if (scrollState.scrollByOutSide) return
    afterObserver()
})

const afterObserver = debounce(() => {
    const elementList = Array.from(elementsMap)
    let header = elementList.filter(item => headersMap.value.get(item[0])).find(([, { show }]) => show)?.[0]
    if (!header) {
        const index = elementList.findIndex(([, { show }]) => show)
        if (index === -1) return
        for (let i = index; i >= 0; i--) {
            const getItem = headersMap.value.get(elementList[i][0])
            if (getItem) {
                header = getItem.dom
                break
            }
        }
    }

    if (header) {
        if (header.id === props.mvvmScrollTo) return
        scrollState.scrollBySelf = true
        emits('update:mvvmScrollTo', header.id)
    }
}, 500)
const setHeadersAndObserver = () => {
    if (!props.ReadOnly) return
    state.headers = editorRef.value.getElemsByTypePrefix('header').map(header => {
        const dom = shadowRoot.getElementById(header.id)
        return {
            ...header,
            text: header.children.map(item => item.text).join(''),
            dom
        }
    }).filter(item => item.text)
    emits('change', { headers: state.headers })

    elementsMap.clear()
    observer.disconnect()
    shadowRoot.querySelectorAll('[data-slate-node="element"]')?.forEach(dom => {
        elementsMap.set(dom, { show: false })
        observer.observe(dom)
    })
}

let editorValue = props.mvvmHTML
const input = (html: string) => {
    editorValue = html
    emits('update:mvvmHTML', html)
    emits('update:mvvmText', editorRef.value?.getText())
}

const renderFn = () => {
    const vnode = h('div', {
        style: {
            border: props.ReadOnly ? 'none' : '1px solid #ccc',
            width: '100%',
            height: '100%',
            display: 'flex',
            'flex-direction': 'column'
        },
        class: {
            'read-only': props.ReadOnly
        }
    }, [
        h('style', {
            textContent: editorCss + `
            .w-e-full-screen-container {
                z-index: 3000;
            }
            .read-only .w-e-text-container [data-slate-editor] {
                padding: 0;
            }
            .w-e-text-container [data-slate-editor] img {
                cursor: zoom-in;
            }
            .read-only .w-e-scroll {
                overflow: unset !important;
            }
            .read-only .w-e-image-container .w-e-image-dragger {
                display: none;
            }
            .w-e-image-container {
                width: ${ props.ImgWidth };
            }
            .w-e-image-container img {
                height: auto !important;
            }
            `
        }),
        h(Toolbar, {
            style: {
                'border-bottom': props.ReadOnly ? 'none' : '1px solid #ccc'
            },
            editor: editorRef.value,
            defaultConfig: toolbarConfig,
            mode
        }),
        h(Editor, {
            style: 'flex: 1; overflow-y: hidden;',
            modelValue: props.mvvmHTML,
            'onUpdate:modelValue': input,
            defaultConfig: editorConfig,
            mode,
            onOnCreated: handleCreated
        })
    ])
    render(vnode, shadowRoot)
}

const imgPreviewRef = ref<HTMLImageElement>()
const addEventListener = () => {
    // 滚动缩放预览图片；预览时禁止页面滚动
    document.addEventListener('wheel', (e) => {
        if (!state.imgContainerVisible) return
        e.preventDefault()

        if (e.deltaY < 0) { // 向上滚动
            zoom(imgPreview.transform.level + 0.4)
        } else { // 向下滚动
            zoom(imgPreview.transform.level - 0.4)
        }
    }, { passive: false })

    // 拖拽预览图片
    const org = {
        x: 0,
        y: 0,
        x2: 0,
        y2: 0,
        left: 0,
        top: 0,
        length: 0,
        level: 0
    }
    const start = (e: MouseEvent | TouchEvent) => {
        if (e.target !== imgPreviewRef.value) return
        if (e instanceof MouseEvent) {
            org.x = e.clientX
            org.y = e.clientY
        } else if (e instanceof TouchEvent) {
            e.preventDefault()
            if (imgPreview.dragStatus === 0) return
            if ([1, 2].includes(e.touches.length)) {
                org.x = e.touches[0].clientX
                org.y = e.touches[0].clientY

                org.x2 = e.touches[1]?.clientX || 0
                org.y2 = e.touches[1]?.clientY || 0

                org.length = Math.hypot(org.x2 - org.x, org.y2 - org.y)

                org.level = imgPreview.transform.level
            }

        }

        org.left = imgPreview.style.left
        org.top = imgPreview.style.top

        imgPreview.dragStatus = 2

        imgPreview.style.transition = '0s'

        document.addEventListener('mousemove', moving)
        document.addEventListener('mouseup', end)

        document.addEventListener('touchmove', moving, { passive: false })
        document.addEventListener('touchend', end)
    }

    const moving = (e: MouseEvent | TouchEvent) => {
        let eventX, eventY
        if (e instanceof MouseEvent) {
            eventX = e.clientX
            eventY = e.clientY
        } else if (e instanceof TouchEvent) {
            e.preventDefault()
            if (e.touches.length === 1) {
                eventX = e.touches[0].clientX
                eventY = e.touches[0].clientY
            } else if (e.touches.length === 2) {
                const lengthDiff = Math.round(Math.hypot(e.touches[1].clientX - e.touches[0].clientX, e.touches[1].clientY - e.touches[0].clientY) - org.length)
                zoom(lengthDiff / 200 + org.level)
                return
            }
        }
        if (!eventX || !eventY) return
        imgPreview.style.left = org.left + eventX - org.x
        imgPreview.style.top = org.top + eventY - org.y
    }

    const end = (e: MouseEvent | TouchEvent) => {
        e.stopPropagation()
        imgPreview.style.transition = ''
        document.removeEventListener('mousemove', moving)
        document.removeEventListener('mouseup', end)

        document.removeEventListener('touchmove', moving)
        document.removeEventListener('touchend', end)
        setTimeout(() => {
            imgPreview.dragStatus = 1
        })
    }
    // PC
    document.addEventListener('mousedown', start)

    // Mobile
    document.addEventListener('touchstart', start, { passive: false })
}

onMounted(() => {
    console.log('wangEditor Init')
    shadowRoot = container.value.attachShadow({ mode: 'open' })
    renderFn()
    addEventListener()
})
</script>
<style lang="scss" scoped>
.wang-editor-img-preview-container {
    position: fixed;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    z-index: 999999;
    overflow: hidden;
    transition: 0.4s;
    background-color: #00000073;
    opacity: 0;
    .tool-bar {
        height: 40px;
        background: #0000001a;
        display: flex;
        align-items: center;
        justify-content: flex-end;
        position: absolute;
        left: 0;
        top: 0;
        width: 100%;
        z-index: 2;
        .operate-icon-container {
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            width: 45px;
            height: 40px;
            transition: 0.4s;
            user-select: none;
            &.disabled {
                opacity: 0.4;
                cursor: not-allowed;
            }
            &:hover {
                background-color: #00000030;
            }
            img {
                width: 14px;
                height: 14px;
            }
        }
    }
    .img-container {
        width: 100%;
        height: 100%;
        z-index: 1;
        :deep(img) {
            position: relative;
            object-fit: contain;
            cursor: grab;
            transition: 0.4s;
            user-select: none;
        }
    }
}
</style>
